{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "sxdnLVT31nfd"
      },
      "source": [
        "![](https://europe-west1-atp-views-tracker.cloudfunctions.net/working-analytics?notebook=tutorials--agent-memory-with-redis--agent-memory-tutorial)\n",
        "\n",
        "# Agent Memory with Redis\n",
        "\n",
        "## 🚀 Run in Google Colab\n",
        "\n",
        "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/NirDiamant/agents-towards-production/blob/main/tutorials/agent-memory-with-redis/agent_memory_tutorial.ipynb)\n",
        "\n",
        "\n",
        "## Introduction\n",
        "\n",
        "Without memory, AI agents are like goldfish - they forget everything after each conversation and can't learn from past interactions or maintain context across sessions. Agentic systems require both **short-term** and **long-term** memory in order to complete tasks in a personalized and resilient manner. Memory is all about state management and [**Redis**](https://redis.io/try-free/?utm_source=nir&utm_medium=cpa&utm_campaign=2025-05-ai_in_production-influencer-nir&utm_content=sd-software_download-7013z000001WaRY) is the well-known in-memory database for exactly this kind of use case today in production systems.\n",
        "\n",
        "## What We'll Build\n",
        "\n",
        "This tutorial demonstrates how to build a **memory-enabled travel agent** with **Redis** and **LangGraph** that remembers user preferences and provides personalized recommendations. This is a **horizontal concept** that you can take and apply to your own agent use cases.\n",
        "\n",
        "We'll explore:\n",
        "\n",
        "1. Short-term memory management using LangGraph's checkpointer\n",
        "2. Long-term memory storage and retrieval using RedisVL\n",
        "3. Managing long-term memory as a tool for a ReAct agent\n",
        "4. Managing conversation history size with summarization"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Ee3ltHdVvKOD"
      },
      "source": [
        "# 🧠 Memory architecture overview\n",
        "\n",
        "Our agent uses a dual-memory system:\n",
        "- **Short-term**: Manages conversation context\n",
        "- **Long-term**: Stores persistent knowledge\n",
        "\n",
        "## Short-term Memory\n",
        "The agent tracks chat history using Redis through LangGraph's [checkpointer](https://github.com/redis-developer/langgraph-redis). Each node in the graph (Retrieve Memories, Respond, Summarize) saves its state to Redis, including conversation history and thread metadata.\n",
        "\n",
        "<img src=\"assets/short-term-memory.png\" width=\"400\" alt=\"Short-term Memory Diagram\">\n",
        "\n",
        "To prevent context window pollution, the agent summarizes conversations when they exceed a configurable length.\n",
        "\n",
        "## Long-term Memory\n",
        "\n",
        "Long-term memories are stored & indexed in Redis using the RedisVL client, with two types:\n",
        "- **Episodic**: User preferences and experiences\n",
        "- **Semantic**: General travel knowledge\n",
        "\n",
        "<img src=\"assets/long-term-memory.png\" width=\"600\" alt=\"Long-term Memory Diagram\">\n",
        "\n",
        ">**NOTE**: These memory types align with the [CoALA](https://arxiv.org/abs/2309.02427) paper's concepts. Our agent's procedural memory is encoded in its Python workflow."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "0KciGua91nfe"
      },
      "source": [
        "---\n",
        "\n",
        "# Set up our environment\n",
        "\n",
        "Before diving into the code, let's set up our development environment with the right Python libraries.\n",
        "\n",
        ">**NOTE**: You may need to restart your kernel after installing libraries."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "0zTUm35H1nfe"
      },
      "outputs": [],
      "source": [
        "%pip install langchain-openai langgraph-checkpoint langgraph langgraph-checkpoint-redis langchain-redis"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8R1hEM7s1nff"
      },
      "source": [
        "## Required API keys\n",
        "\n",
        "You must add an [OpenAI API](https://platform.openai.com/signup) key with billing information for this tutorial."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "365fzPsj1nff"
      },
      "outputs": [],
      "source": [
        "import getpass\n",
        "import os\n",
        "\n",
        "def _set_env(key: str):\n",
        "    if key not in os.environ:\n",
        "        os.environ[key] = getpass.getpass(f\"{key}:\")\n",
        "\n",
        "\n",
        "_set_env(\"OPENAI_API_KEY\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "NLkF4GB_1nff"
      },
      "source": [
        "## Setup Redis\n",
        "\n",
        "You have two options for running Redis:\n",
        "\n",
        "1. **Redis Cloud**: For a fully-managed, seamless experience, use [a free instance of Redis Cloud](https://redis.io/try-free/?utm_source=nir&utm_medium=cpa&utm_campaign=2025-05-ai_in_production-influencer-nir&utm_content=sd-software_download-7013z000001WaRY).\n",
        "2. **Local Redis**: For a simple, local (non-persistent) Redis instance, run the cell below."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "zgKbb4ol1nff"
      },
      "source": [
        "Run the cell below to get a localized Redis instance on your Google colab server."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "xs7bi1kr1nff"
      },
      "outputs": [],
      "source": [
        "%%sh\n",
        "curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg\n",
        "echo \"deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/redis.list\n",
        "sudo apt-get update  > /dev/null 2>&1\n",
        "sudo apt-get install redis-stack-server  > /dev/null 2>&1\n",
        "redis-stack-server --daemonize yes"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "-B8XRKHR1nff"
      },
      "source": [
        "Let's test out Redis connection and create a client to communicate with the server."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dauPT3PT1nff"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "\n",
        "from redis import Redis\n",
        "\n",
        "# Use the environment variable if set, otherwise default to localhost\n",
        "REDIS_URL = os.getenv(\"REDIS_URL\", \"redis://localhost:6379\")\n",
        "\n",
        "redis_client = Redis.from_url(REDIS_URL)\n",
        "redis_client.ping()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "aRxYTTOf1nfg"
      },
      "source": [
        "## Prepare memory data models\n",
        "\n",
        "In this section, we'll create a robust data modeling system for our agent's memory using `Pydantic`. These models will ensure type safety and provide clear data structures for storing and retrieving memories from Redis.\n",
        "\n",
        "We'll implement four key components:\n",
        "\n",
        "1. `MemoryType` - An enumeration that categorizes memories into two types:\n",
        "   - Episodic: Personal experiences and user preferences\n",
        "   - Semantic: General knowledge and domain facts\n",
        "\n",
        "2. `Memory` - The core model representing a single memory entry with its content and metadata\n",
        "\n",
        "3. `Memories` - A container model that holds collections of memory objects\n",
        "\n",
        "4. `StoredMemory` - A specialized model for memories that have been persisted to Redis\n",
        "\n",
        "These models work together to create a complete memory lifecycle, from creation to storage and retrieval."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "id": "Ix6Pe6qG1nfg"
      },
      "outputs": [],
      "source": [
        "import ulid\n",
        "\n",
        "from datetime import datetime\n",
        "from enum import Enum\n",
        "from typing import List, Optional\n",
        "from pydantic import BaseModel, Field\n",
        "\n",
        "\n",
        "class MemoryType(str, Enum):\n",
        "    \"\"\"\n",
        "    Defines the type of long-term memory for categorization and retrieval.\n",
        "\n",
        "    EPISODIC: Personal experiences and user-specific preferences\n",
        "              (e.g., \"User prefers Delta airlines\", \"User visited Paris last year\")\n",
        "\n",
        "    SEMANTIC: General domain knowledge and facts\n",
        "              (e.g., \"Singapore requires passport\", \"Tokyo has excellent public transit\")\n",
        "\n",
        "    The type of a long-term memory.\n",
        "\n",
        "    EPISODIC: User specific experiences and preferences\n",
        "\n",
        "    SEMANTIC: General knowledge on top of the user's preferences and LLM's\n",
        "    training data.\n",
        "    \"\"\"\n",
        "\n",
        "    EPISODIC = \"episodic\"\n",
        "    SEMANTIC = \"semantic\"\n",
        "\n",
        "\n",
        "class Memory(BaseModel):\n",
        "    \"\"\"Represents a single long-term memory.\"\"\"\n",
        "\n",
        "    content: str\n",
        "    memory_type: MemoryType\n",
        "    metadata: str\n",
        "\n",
        "\n",
        "class Memories(BaseModel):\n",
        "    \"\"\"\n",
        "    A list of memories extracted from a conversation by an LLM.\n",
        "\n",
        "    NOTE: OpenAI's structured output requires us to wrap the list in an object.\n",
        "    \"\"\"\n",
        "\n",
        "    memories: List[Memory]\n",
        "\n",
        "\n",
        "class StoredMemory(Memory):\n",
        "    \"\"\"A stored long-term memory\"\"\"\n",
        "\n",
        "    id: str  # The redis key\n",
        "    memory_id: ulid.ULID = Field(default_factory=lambda: ulid.ULID())\n",
        "    created_at: datetime = Field(default_factory=datetime.now)\n",
        "    user_id: Optional[str] = None\n",
        "    thread_id: Optional[str] = None\n",
        "    memory_type: Optional[MemoryType] = None"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "P6a03f4b1nfg"
      },
      "source": [
        "Now we have type-safe data models that handle the complete memory lifecycle from LLM extraction to Redis storage, with proper metadata tracking for production use. Next, we'll set up the Redis infrastructure to store and search these memories using vector embeddings."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "T0FBUdRY1nfg"
      },
      "source": [
        "# Memory Storage\n",
        "\n",
        "- **Short-term memory** is handled automatically by `RedisSaver` from `langgraph-checkpoint-redis`.\n",
        "- For **long-term memory**, we'll use RedisVL with vector embeddings to enable semantic search of past experiences and knowledge.\n",
        "\n",
        "Below, we will create a search index schema in Redis to hold our long term memories. The schema has a few different fields including content, memory type, metadata, timestamps, user id, memory id, and the embedding of the memory."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "id": "D-bfk_Ro1nfg"
      },
      "outputs": [],
      "source": [
        "from redisvl.index import SearchIndex\n",
        "from redisvl.schema.schema import IndexSchema\n",
        "\n",
        "\n",
        "# Define the schema for our vector search index\n",
        "# This creates the structure for storing and querying memories\n",
        "memory_schema = IndexSchema.from_dict({\n",
        "        \"index\": {\n",
        "            \"name\": \"agent_memories\",  # Index name for identification\n",
        "            \"prefix\": \"memory\",       # Redis key prefix (memory:1, memory:2, etc.)\n",
        "            \"key_separator\": \":\",\n",
        "            \"storage_type\": \"json\",\n",
        "        },\n",
        "        \"fields\": [\n",
        "            {\"name\": \"content\", \"type\": \"text\"},\n",
        "            {\"name\": \"memory_type\", \"type\": \"tag\"},\n",
        "            {\"name\": \"metadata\", \"type\": \"text\"},\n",
        "            {\"name\": \"created_at\", \"type\": \"text\"},\n",
        "            {\"name\": \"user_id\", \"type\": \"tag\"},\n",
        "            {\"name\": \"memory_id\", \"type\": \"tag\"},\n",
        "            {\n",
        "                \"name\": \"embedding\",\n",
        "                \"type\": \"vector\",\n",
        "                \"attrs\": {\n",
        "                    \"algorithm\": \"flat\",\n",
        "                    \"dims\": 1536,  # OpenAI embedding dimension\n",
        "                    \"distance_metric\": \"cosine\",\n",
        "                    \"datatype\": \"float32\",\n",
        "                },\n",
        "            },\n",
        "        ],\n",
        "    }\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "IHUC6A6tvKOF"
      },
      "source": [
        "Below we create the `SearchIndex` from the `IndexSchema` and our Redis client connection object. We will overwrite the index spec if its already created!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "iMHgajwyvKOF",
        "outputId": "bc3892c0-6139-4458-e79d-de2249d1da0d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Long-term memory index ready\n"
          ]
        }
      ],
      "source": [
        "try:\n",
        "    long_term_memory_index = SearchIndex(\n",
        "        schema=memory_schema,\n",
        "        redis_client=redis_client,\n",
        "        validate_on_load=True\n",
        "    )\n",
        "    long_term_memory_index.create(overwrite=True)\n",
        "    print(\"Long-term memory index ready\")\n",
        "except Exception as e:\n",
        "    print(f\"Error creating index: {e}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "q9J3oIwN24M-"
      },
      "source": [
        "Now that the index is created, we can inspect the long term memory index in Redis using the `rvl` cli:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "smnQbc5-2y_C",
        "outputId": "221e0ccd-3857-4983-d500-5095a075e601"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "Index Information:\n",
            "╭────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬\b╮\n",
            "│ Index Name     │ Storage Type   │ Prefixes       │ Index Options  │ Indexing       │\n",
            "├────────────────┼────────────────┼────────────────┼────────────────┼────────────────┼\b┤\n",
            "| agent_memories | JSON           | ['memory']     | []             | 0              |\n",
            "╰────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴\b╯\n",
            "Index Fields:\n",
            "╭─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬\b╮\n",
            "│ Name            │ Attribute       │ Type            │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │ Field Option    │ Option Value    │\n",
            "├─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼\b┤\n",
            "│ $.content       │ content         │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │\n",
            "│ $.memory_type   │ memory_type     │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │\n",
            "│ $.metadata      │ metadata        │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │\n",
            "│ $.created_at    │ created_at      │ TEXT            │ WEIGHT          │ 1               │                 │                 │                 │                 │                 │                 │\n",
            "│ $.user_id       │ user_id         │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │\n",
            "│ $.memory_id     │ memory_id       │ TAG             │ SEPARATOR       │ ,               │                 │                 │                 │                 │                 │                 │\n",
            "│ $.embedding     │ embedding       │ VECTOR          │ algorithm       │ FLAT            │ data_type       │ FLOAT32         │ dim             │ 1536            │ distance_metric │ COSINE          │\n",
            "╰─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴─────────────────┴\b╯\n"
          ]
        }
      ],
      "source": [
        "!rvl index info -i agent_memories"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "r5ybTN2l1nfg"
      },
      "source": [
        "## Functions to access memories\n",
        "\n",
        "Next, we provide three core functions to access, store and retrieve memories. We will eventually use these in tools for the LLM to call. We will start by loading a vectorizer class to create OpenAI embeddings.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "metadata": {
        "id": "ruYpDU_lvKOF"
      },
      "outputs": [],
      "source": [
        "from redisvl.utils.vectorize.text.openai import OpenAITextVectorizer\n",
        "\n",
        "openai_embed = OpenAITextVectorizer(model=\"text-embedding-ada-002\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HXLu70owvKOF"
      },
      "source": [
        "Next we will set up a simple logger so our functions will record log activity of what's happening."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {
        "id": "-XIpiadMvKOF"
      },
      "outputs": [],
      "source": [
        "import logging\n",
        "\n",
        "# Set up a logger\n",
        "logger = logging.getLogger(__name__)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "eMBbx2MkvKOF"
      },
      "source": [
        "### 1. Check for similar memories\n",
        "First, we'll write a utility function to check if a memory similar to a given\n",
        "memory already exists in the index.\n",
        "\n",
        "This function checks for duplicate memories in Redis by:\n",
        "1. Converting the input content into a vector embedding\n",
        "2. Creating filters for user_id and memory_type\n",
        "3. Using vector similarity search with a vector range query to find any existing + similar memories\n",
        "4. Returning True if a similar memory exists, False otherwise\n",
        "\n",
        "This helps prevent storing redundant information in the agent's memory."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {
        "id": "GN9zPAWO1nfg"
      },
      "outputs": [],
      "source": [
        "from redisvl.query import VectorRangeQuery\n",
        "from redisvl.query.filter import Tag\n",
        "\n",
        "\n",
        "# If we have any memories that aren't associated with a user, we'll use this ID.\n",
        "SYSTEM_USER_ID = \"system\"\n",
        "\n",
        "\n",
        "def similar_memory_exists(\n",
        "    content: str,\n",
        "    memory_type: MemoryType,\n",
        "    user_id: str = SYSTEM_USER_ID,\n",
        "    thread_id: Optional[str] = None,\n",
        "    distance_threshold: float = 0.1,\n",
        ") -> bool:\n",
        "    \"\"\"Check if a similar long-term memory already exists in Redis.\"\"\"\n",
        "    content_embedding = openai_embed.embed(content)\n",
        "\n",
        "    filters = (Tag(\"user_id\") == user_id) & (Tag(\"memory_type\") == memory_type)\n",
        "\n",
        "    if thread_id:\n",
        "        filters = filters & (Tag(\"thread_id\") == thread_id)\n",
        "\n",
        "    # Search for similar memories\n",
        "    vector_query = VectorRangeQuery(\n",
        "        vector=content_embedding,\n",
        "        num_results=1,\n",
        "        vector_field_name=\"embedding\",\n",
        "        filter_expression=filters,\n",
        "        distance_threshold=distance_threshold,\n",
        "        return_fields=[\"id\"],\n",
        "    )\n",
        "    results = long_term_memory_index.query(vector_query)\n",
        "    logger.debug(f\"Similar memory search results: {results}\")\n",
        "\n",
        "    if results:\n",
        "        logger.debug(\n",
        "            f\"{len(results)} similar {'memory' if results.count == 1 else 'memories'} found. First: \"\n",
        "            f\"{results[0]['id']}. Skipping storage.\"\n",
        "        )\n",
        "        return True\n",
        "\n",
        "    return False"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_zqJwlXx1nfg"
      },
      "source": [
        "### 2. Store long-term memories"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "KIu2CrUq1nfg"
      },
      "source": [
        "Below is a function that handles storing long-term memories in Redis with built-in deduplication.\n",
        "\n",
        "It's a key part of our memory system that:\n",
        "1. Prevents duplicate memories by checking for similar content\n",
        "2. Creates vector embeddings for semantic search capabilities\n",
        "3. Stores the memory with relevant metadata for future retrieval\n",
        "\n",
        "We'll use the `similar_memory_exists()` function when we store memories in order to perform in-line memory deduplication."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "metadata": {
        "id": "oKA39Qp21nfh"
      },
      "outputs": [],
      "source": [
        "\n",
        "\n",
        "from datetime import datetime\n",
        "from typing import List, Optional, Union\n",
        "\n",
        "import ulid\n",
        "\n",
        "\n",
        "def store_memory(\n",
        "    content: str,\n",
        "    memory_type: MemoryType,\n",
        "    user_id: str = SYSTEM_USER_ID,\n",
        "    thread_id: Optional[str] = None,\n",
        "    metadata: Optional[str] = None,\n",
        "):\n",
        "    \"\"\"Store a long-term memory in Redis with deduplication.\n",
        "\n",
        "        This function:\n",
        "        1. Checks for similar existing memories to avoid duplicates\n",
        "        2. Generates vector embeddings for semantic search\n",
        "        3. Stores the memory with metadata for retrieval\n",
        "        \"\"\"\n",
        "    if metadata is None:\n",
        "        metadata = \"{}\"\n",
        "\n",
        "    logger.info(f\"Preparing to store memory: {content}\")\n",
        "\n",
        "    if similar_memory_exists(content, memory_type, user_id, thread_id):\n",
        "        logger.info(\"Similar memory found, skipping storage\")\n",
        "        return\n",
        "\n",
        "    embedding = openai_embed.embed(content)\n",
        "\n",
        "    memory_data = {\n",
        "        \"user_id\": user_id or SYSTEM_USER_ID,\n",
        "        \"content\": content,\n",
        "        \"memory_type\": memory_type.value,\n",
        "        \"metadata\": metadata,\n",
        "        \"created_at\": datetime.now().isoformat(),\n",
        "        \"embedding\": embedding,\n",
        "        \"memory_id\": str(ulid.ULID()),\n",
        "        \"thread_id\": thread_id,\n",
        "    }\n",
        "\n",
        "    try:\n",
        "        long_term_memory_index.load([memory_data])\n",
        "    except Exception as e:\n",
        "        logger.error(f\"Error storing memory: {e}\")\n",
        "        return\n",
        "\n",
        "    logger.info(f\"Stored {memory_type} memory: {content}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "0cpk-m7Z1nfh"
      },
      "source": [
        "### 3. Retrieve relevant long-term memories\n",
        "And now that we're storing memories, we can retrieve them using vector similarity search with metadata filters using RedisVL.\n",
        "\n",
        "This function:\n",
        "1. Takes a query string, optional filters (memory type, user ID, thread ID), and a distance threshold (semantic)\n",
        "2. Creates a vector range query using the query's embedding\n",
        "3. Builds a filter object based on passed options\n",
        "4. Filters to narrow down the search results\n",
        "4. Executes the search and returns parsed memory objects"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "metadata": {
        "id": "xuEAMNjq1nfh"
      },
      "outputs": [],
      "source": [
        "def retrieve_memories(\n",
        "    query: str,\n",
        "    memory_type: Union[Optional[MemoryType], List[MemoryType]] = None,\n",
        "    user_id: str = SYSTEM_USER_ID,\n",
        "    thread_id: Optional[str] = None,\n",
        "    distance_threshold: float = 0.1,\n",
        "    limit: int = 5,\n",
        ") -> List[StoredMemory]:\n",
        "    \"\"\"Retrieve relevant memories from Redis using vector similarity search.\n",
        "\n",
        "    \"\"\"\n",
        "    # Create vector query using query embedding\n",
        "    logger.debug(f\"Retrieving memories for query: {query}\")\n",
        "    vector_query = VectorRangeQuery(\n",
        "        vector=openai_embed.embed(query),\n",
        "        return_fields=[\n",
        "            \"content\",\n",
        "            \"memory_type\", \n",
        "            \"metadata\",\n",
        "            \"created_at\",\n",
        "            \"memory_id\",\n",
        "            \"thread_id\",\n",
        "            \"user_id\",\n",
        "        ],\n",
        "        num_results=limit,\n",
        "        vector_field_name=\"embedding\",\n",
        "        dialect=2,\n",
        "        distance_threshold=distance_threshold,\n",
        "    )\n",
        "\n",
        "    # Build filter conditions\n",
        "    base_filters = [f\"@user_id:{{{user_id or SYSTEM_USER_ID}}}\"]\n",
        "\n",
        "    if memory_type:\n",
        "        if isinstance(memory_type, list):\n",
        "            base_filters.append(f\"@memory_type:{{{'|'.join(memory_type)}}}\")\n",
        "        else:\n",
        "            base_filters.append(f\"@memory_type:{{{memory_type.value}}}\")\n",
        "\n",
        "    if thread_id:\n",
        "        base_filters.append(f\"@thread_id:{{{thread_id}}}\")\n",
        "\n",
        "    vector_query.set_filter(\" \".join(base_filters))\n",
        "\n",
        "    # Execute vector similarity search\n",
        "    results = long_term_memory_index.query(vector_query)\n",
        "\n",
        "    # Parse results into StoredMemory objects\n",
        "    memories = []\n",
        "    for doc in results:\n",
        "        try:\n",
        "            memory = StoredMemory(\n",
        "                id=doc[\"id\"],\n",
        "                memory_id=doc[\"memory_id\"],\n",
        "                user_id=doc[\"user_id\"],\n",
        "                thread_id=doc.get(\"thread_id\", None),\n",
        "                memory_type=MemoryType(doc[\"memory_type\"]),\n",
        "                content=doc[\"content\"],\n",
        "                created_at=doc[\"created_at\"],\n",
        "                metadata=doc[\"metadata\"],\n",
        "            )\n",
        "            memories.append(memory)\n",
        "        except Exception as e:\n",
        "            logger.error(f\"Error parsing memory: {e}\")\n",
        "            continue\n",
        "    return memories"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "YinPoLcc1nfh"
      },
      "source": [
        "## 🛠️ Managing Long-Term Memory with Tools\n",
        "\n",
        "Memory operations are exposed as **tools** that the LLM can call to store or retrieve memories.\n",
        "\n",
        "**Tool-based memory management:**\n",
        "- LLM decides when to store/retrieve memories\n",
        "- Fewer Redis calls but may miss some context\n",
        "- Adds some latency due to LLM decision-making\n",
        "\n",
        "Alternatively, you can always manually manage memories in your workflows.\n",
        "\n",
        "**Manual memory management:**\n",
        "- More Redis calls but faster response times\n",
        "- Extracts more memories, providing richer context\n",
        "- Higher token usage due to more context\n",
        "\n",
        "> NOTE: **This tutorial uses tool-based memory** for optimal balance of control and efficiency.\n",
        "\n",
        "<img src=\"assets/memory-agents.png\" width=\"600\" alt=\"Memory Agents Diagram\">"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BmwB-sUJ1nfh"
      },
      "source": [
        "### Define Agent Tools\n",
        "\n",
        "Now that we have our storage functions defined, we can create the tools that will enable our agent to interact with the memory system. These tools will be used by the LLM to manage memories during conversations.\n",
        "\n",
        "Let's start with the Store Memory Tool:\n",
        "\n",
        "#### Store Memory Tool\n",
        "\n",
        "This tool enables the agent to save important information as long-term memories in Redis. It's particularly useful for capturing:\n",
        "- User preferences and habits\n",
        "- Personal experiences and anecdotes\n",
        "- Important facts and knowledge shared during conversations\n",
        "\n",
        "The tool accepts the following parameters:\n",
        "- `content`: The actual memory content to store (e.g., \"User prefers window seats on flights\")\n",
        "- `memory_type`: The type of memory (e.g., `MemoryType.EPISODIC` for personal experiences, `MemoryType.SEMANTIC` for general knowledge)\n",
        "- `metadata`: Optional dictionary for additional context (e.g., timestamps, source, confidence)\n",
        "- `config`: Optional configuration for user/thread context (automatically handled by the agent)\n",
        "\n",
        "When called, the tool:\n",
        "1. Validates the input parameters\n",
        "2. Stores the memory in Redis with proper indexing\n",
        "3. Returns a success message with the stored content\n",
        "4. Handles errors gracefully with informative messages\n",
        "\n",
        "This tool is designed to be used by the LLM to build a persistent memory of the user's preferences and experiences, enabling more personalized and context-aware interactions over time."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "metadata": {
        "id": "T-S0eN4B1nfh"
      },
      "outputs": [],
      "source": [
        "from typing import Dict, Optional\n",
        "\n",
        "from langchain_core.tools import tool\n",
        "from langchain_core.runnables.config import RunnableConfig\n",
        "\n",
        "\n",
        "@tool\n",
        "def store_memory_tool(\n",
        "    content: str,\n",
        "    memory_type: MemoryType,\n",
        "    metadata: Optional[Dict[str, str]] = None,\n",
        "    config: Optional[RunnableConfig] = None,\n",
        ") -> str:\n",
        "    \"\"\"\n",
        "    Store a long-term memory in the system.\n",
        "\n",
        "    Use this tool to save important information about user preferences,\n",
        "    experiences, or general knowledge that might be useful in future\n",
        "    interactions.\n",
        "    \"\"\"\n",
        "    config = config or RunnableConfig()\n",
        "    user_id = config.get(\"user_id\", SYSTEM_USER_ID)\n",
        "    thread_id = config.get(\"thread_id\")\n",
        "\n",
        "    try:\n",
        "        # Store in long-term memory\n",
        "        store_memory(\n",
        "            content=content,\n",
        "            memory_type=memory_type,\n",
        "            user_id=user_id,\n",
        "            thread_id=thread_id,\n",
        "            metadata=str(metadata) if metadata else None,\n",
        "        )\n",
        "\n",
        "        return f\"Successfully stored {memory_type} memory: {content}\"\n",
        "    except Exception as e:\n",
        "        return f\"Error storing memory: {str(e)}\"\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9Am1Z_hItKpc"
      },
      "source": [
        "Test the tool:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 35
        },
        "id": "H1-HPwag-im_",
        "outputId": "4b883edc-29e2-4666-84ae-4e156b03661c"
      },
      "outputs": [
        {
          "data": {
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            },
            "text/plain": [
              "'Successfully stored MemoryType.EPISODIC memory: I like flying on Delta when possible'"
            ]
          },
          "execution_count": 16,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "store_memory_tool.invoke({\"content\": \"I like flying on Delta when possible\", \"memory_type\": \"episodic\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MkjIWht9vKOG"
      },
      "source": [
        "Now that we've seen how to store memories, let's look at how to retrieve them.\n",
        "\n",
        "#### Retrieve Memories Tool\n",
        "This tool allows us to search through our stored memories using semantic similarity and filtering.\n",
        "\n",
        "This tool is particularly useful when you want to:\n",
        "- Find relevant past experiences or preferences\n",
        "- Filter memories by type (episodic or semantic)\n",
        "- Get user-specific information\n",
        "- Limit the number of results to keep responses focused\n",
        "\n",
        "The tool works by:\n",
        "1. Taking a query string and searching for semantically similar memories\n",
        "2. Filtering results based on memory type\n",
        "3. Applying a similarity threshold to ensure relevance\n",
        "4. Formatting the results in a clear, readable way"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "metadata": {
        "id": "NEqm-q1ovKOG"
      },
      "outputs": [],
      "source": [
        "@tool\n",
        "def retrieve_memories_tool(\n",
        "    query: str,\n",
        "    memory_type: List[MemoryType],\n",
        "    limit: int = 5,\n",
        "    config: Optional[RunnableConfig] = None,\n",
        ") -> str:\n",
        "    \"\"\"\n",
        "    Retrieve long-term memories relevant to the query.\n",
        "\n",
        "    Use this tool to access previously stored information about user\n",
        "    preferences, experiences, or general knowledge.\n",
        "    \"\"\"\n",
        "    config = config or RunnableConfig()\n",
        "    user_id = config.get(\"user_id\", SYSTEM_USER_ID)\n",
        "\n",
        "    try:\n",
        "        # Get long-term memories\n",
        "        stored_memories = retrieve_memories(\n",
        "            query=query,\n",
        "            memory_type=memory_type,\n",
        "            user_id=user_id,\n",
        "            limit=limit,\n",
        "            distance_threshold=0.3,\n",
        "        )\n",
        "\n",
        "        # Format the response\n",
        "        response = []\n",
        "\n",
        "        if stored_memories:\n",
        "            response.append(\"Long-term memories:\")\n",
        "            for memory in stored_memories:\n",
        "                response.append(f\"- [{memory.memory_type}] {memory.content}\")\n",
        "\n",
        "        return \"\\n\".join(response) if response else \"No relevant memories found.\"\n",
        "\n",
        "    except Exception as e:\n",
        "        return f\"Error retrieving memories: {str(e)}\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4irYew3pvKON"
      },
      "source": [
        "Test the tool:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 35
        },
        "id": "CMlAHmTe9vhN",
        "outputId": "95304a90-39c3-42d3-bcdc-d7d6ea6e2191"
      },
      "outputs": [
        {
          "data": {
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            },
            "text/plain": [
              "'Long-term memories:\\n- [MemoryType.EPISODIC] I like flying on Delta when possible'"
            ]
          },
          "execution_count": 17,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "retrieve_memories_tool.invoke({\"query\": \"Airline preferences\", \"memory_type\": [\"episodic\"]})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "PftV2tTG1nfh"
      },
      "source": [
        "# 🌎 Build the Travel Agent\n",
        "\n",
        "## Setting Up the ReAct Agent\n",
        "\n",
        "We'll use LangGraph's prebuilt components to create a ReAct agent with memory capabilities:\n",
        "\n",
        "1. **Short-term Memory**: A checkpoint saver tracks conversation history per thread\n",
        "2. **Long-term Memory**: We'll extract and store key information from conversations\n",
        "   - Episodic memories: User preferences and experiences\n",
        "   - Semantic memories: General travel knowledge\n",
        "\n",
        "The system will automatically summarize conversations to manage context while preserving important details in long-term storage.\n",
        "\n",
        "Below we start with setting up the Redis checkpointer (`RedisSaver`) that will handle short term memory for the agent."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "metadata": {
        "id": "QSouau_jvKON"
      },
      "outputs": [],
      "source": [
        "from langchain_core.messages import AIMessage, SystemMessage\n",
        "from langchain_openai import ChatOpenAI\n",
        "from langgraph.prebuilt.chat_agent_executor import create_react_agent\n",
        "from langgraph.checkpoint.redis import RedisSaver\n",
        "\n",
        "# Set up the Redis checkpointer for short term memory\n",
        "redis_saver = RedisSaver(redis_client=redis_client)\n",
        "redis_saver.setup()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "a8LEro_PvKON"
      },
      "source": [
        "Next we define the set of tools for the agent."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "metadata": {
        "id": "EtZo92KuvKON"
      },
      "outputs": [],
      "source": [
        "# Define the set of tools\n",
        "tools = [store_memory_tool, retrieve_memories_tool]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "e-2IMJaLvKON"
      },
      "source": [
        "Configure the LLM from OpenAI."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "metadata": {
        "id": "kWz7rC5_vKON"
      },
      "outputs": [],
      "source": [
        "# Configure an LLM for the agent with a more creative temperature.\n",
        "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0.7).bind_tools(tools)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "JLKB-V9HvKON"
      },
      "source": [
        "Assemble the ReAct agent combining the LLM, tools, checkpointer, and system prompt!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "metadata": {
        "id": "e-TpYxYb1nfh"
      },
      "outputs": [],
      "source": [
        "# Define the travel agent\n",
        "travel_agent = create_react_agent(\n",
        "    model=llm,\n",
        "    tools=tools,               # Long-term memory: provided as a set of custom tools\n",
        "    checkpointer=redis_saver,  # Short-term memory: the conversation history\n",
        "    prompt=SystemMessage(\n",
        "        content=\"\"\"\n",
        "        You are a travel assistant helping users plan their trips. You remember user preferences\n",
        "        and provide personalized recommendations based on past interactions.\n",
        "\n",
        "        You have access to the following types of memory:\n",
        "        1. Short-term memory: The current conversation thread\n",
        "        2. Long-term memory:\n",
        "           - Episodic: User preferences and past trip experiences (e.g., \"User prefers window seats\")\n",
        "           - Semantic: General knowledge about travel destinations and requirements\n",
        "\n",
        "        Your procedural knowledge (how to search, book flights, etc.) is built into your tools and prompts.\n",
        "\n",
        "        Always be helpful, personal, and context-aware in your responses.\n",
        "        \"\"\"\n",
        "    ),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "htuJmhkY1nfi"
      },
      "source": [
        "✅ Now that we have the basic agent in place, we will build a LangGraph workflow that invokes this agent as a node. The graph will consist of three nodes in total. We will move through each one separately."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "R2mZwvHj1nfi"
      },
      "source": [
        "## Node 1: Respond to the user\n",
        "In LangGraph, a **node** represents a discrete unit of processing in a workflow. Each node is a function that takes a state object and configuration as input, processes the data, and returns an updated state. Nodes can be connected to form a directed graph that defines the flow of execution.\n",
        "\n",
        "The `respond_to_user` node (below) is our first node in the travel agent workflow. It serves as the entry point for user interactions and handles the core conversation flow. Here's how it works:\n",
        "\n",
        "1. It receives the current conversation state and configuration\n",
        "2. Extracts any human messages from the state\n",
        "3. Invokes our travel agent to generate a response\n",
        "4. Handles any errors gracefully\n",
        "5. Updates the conversation state with the agent's response\n",
        "\n",
        "The node uses a custom `RuntimeState` class that inherits from `MessagesState` to maintain the conversation history. This state object is passed between nodes in the graph, allowing each node to access and modify the conversation context as needed."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "metadata": {
        "id": "PFdGi8fd1nfi"
      },
      "outputs": [],
      "source": [
        "\n",
        "from langchain_core.messages import HumanMessage\n",
        "from langgraph.graph.message import MessagesState\n",
        "\n",
        "\n",
        "class RuntimeState(MessagesState):\n",
        "    \"\"\"Runtime state for the travel agent.\"\"\"\n",
        "    pass\n",
        "\n",
        "\n",
        "def respond_to_user(state: RuntimeState, config: RunnableConfig) -> RuntimeState:\n",
        "    \"\"\"Invoke the travel agent to generate a response.\"\"\"\n",
        "    human_messages = [m for m in state[\"messages\"] if isinstance(m, HumanMessage)]\n",
        "    if not human_messages:\n",
        "        logger.warning(\"No HumanMessage found in state\")\n",
        "        return state\n",
        "\n",
        "    try:\n",
        "        # Single agent invocation, not streamed (simplified for reliability)\n",
        "        result = travel_agent.invoke({\"messages\": state[\"messages\"]}, config=config)\n",
        "        agent_message = result[\"messages\"][-1]\n",
        "        state[\"messages\"].append(agent_message)\n",
        "    except Exception as e:\n",
        "        logger.error(f\"Error invoking travel agent: {e}\")\n",
        "        agent_message = AIMessage(\n",
        "            content=\"I'm sorry, I encountered an error processing your request.\"\n",
        "        )\n",
        "        state[\"messages\"].append(agent_message)\n",
        "\n",
        "    return state"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "kZyQE3MeyoQw"
      },
      "source": [
        "## Node 2: Execute Tools\n",
        "\n",
        "The `execute_tools` node is a critical component in our travel agent's workflow that bridges the gap between the LLM's decisions and actual tool execution. Positioned after the `respond_to_user` node, it handles the practical side of the agent's tool-using capabilities.\n",
        "\n",
        "When the LLM determines it needs to use a tool, it includes tool calls in its response. This node then:\n",
        "\n",
        "1. Scans the conversation history to find the most recent AI message containing tool calls\n",
        "2. For each tool call found:\n",
        "   - Extracts the tool name, arguments, and call ID from the message\n",
        "   - Matches the tool name against our available tools\n",
        "   - Executes the tool with the provided arguments\n",
        "   - Creates a ToolMessage containing the result\n",
        "3. Handles any errors that occur during tool execution\n",
        "4. Adds all tool results back to the conversation history\n",
        "\n",
        "This node is essential because it enables our agent to interact with external systems and services while maintaining a coherent conversation flow. Without it, the agent would be limited to just generating text responses without the ability to perform actual actions or retrieve real-time information.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "metadata": {
        "id": "bkFA2_vgyrdZ"
      },
      "outputs": [],
      "source": [
        "from langchain_core.messages import ToolMessage\n",
        "\n",
        "\n",
        "def execute_tools(state: RuntimeState, config: RunnableConfig) -> RuntimeState:\n",
        "    \"\"\"Execute tools specified in the latest AIMessage and append ToolMessages.\"\"\"\n",
        "    messages = state[\"messages\"]\n",
        "    latest_ai_message = next(\n",
        "        (m for m in reversed(messages) if isinstance(m, AIMessage) and m.tool_calls),\n",
        "        None\n",
        "    )\n",
        "\n",
        "    if not latest_ai_message:\n",
        "        return state  # No tool calls to process\n",
        "\n",
        "    tool_messages = []\n",
        "    for tool_call in latest_ai_message.tool_calls:\n",
        "        tool_name = tool_call[\"name\"]\n",
        "        tool_args = tool_call[\"args\"]\n",
        "        tool_id = tool_call[\"id\"]\n",
        "\n",
        "        # Find the corresponding tool\n",
        "        tool = next((t for t in tools if t.name == tool_name), None)\n",
        "        if not tool:\n",
        "            continue  # Skip if tool not found\n",
        "\n",
        "        try:\n",
        "            # Execute the tool with the provided arguments\n",
        "            result = tool.invoke(tool_args, config=config)\n",
        "            # Create a ToolMessage with the result\n",
        "            tool_message = ToolMessage(\n",
        "                content=str(result),\n",
        "                tool_call_id=tool_id,\n",
        "                name=tool_name\n",
        "            )\n",
        "            tool_messages.append(tool_message)\n",
        "        except Exception as e:\n",
        "            # Handle tool execution errors\n",
        "            error_message = ToolMessage(\n",
        "                content=f\"Error executing tool '{tool_name}': {str(e)}\",\n",
        "                tool_call_id=tool_id,\n",
        "                name=tool_name\n",
        "            )\n",
        "            tool_messages.append(error_message)\n",
        "\n",
        "    # Append the ToolMessages to the message history\n",
        "    messages.extend(tool_messages)\n",
        "    state[\"messages\"] = messages\n",
        "    return state"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "LM3oPg101nfi"
      },
      "source": [
        "## Node 3: Conversation Summarization\n",
        "\n",
        "The conversation summarization node helps manage context by condensing chat history into concise summaries. This prevents the LLM's context window from being overwhelmed as the conversation grows.\n",
        "\n",
        "Key features:\n",
        "\n",
        "1. **Automatic Triggering**: Summarizes after every 6 messages (configurable)\n",
        "\n",
        "2. **Smart Summarization**:\n",
        "   - Uses GPT-4o with low temperature (0.3) for consistent summaries\n",
        "   - Preserves key information like preferences and pending items\n",
        "   - Replaces old messages while keeping recent context\n",
        "\n",
        "3. **Benefits**:\n",
        "   - Prevents context window overflow\n",
        "   - Maintains conversation coherence\n",
        "   - Optimizes token usage\n",
        "\n",
        "The summary becomes part of the conversation history, allowing the agent to reference past interactions efficiently."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "metadata": {
        "id": "KUYw18Xb1nfi"
      },
      "outputs": [],
      "source": [
        "from langchain_core.messages import RemoveMessage\n",
        "\n",
        "# An LLM configured for summarization.\n",
        "summarizer = ChatOpenAI(model=\"gpt-4o\", temperature=0.3)\n",
        "\n",
        "# The number of messages after which we'll summarize the conversation.\n",
        "MESSAGE_SUMMARIZATION_THRESHOLD = 6\n",
        "\n",
        "\n",
        "def summarize_conversation(\n",
        "    state: RuntimeState, config: RunnableConfig\n",
        ") -> RuntimeState:\n",
        "    \"\"\"\n",
        "    Summarize a list of messages into a concise summary to reduce context length\n",
        "    while preserving important information.\n",
        "    \"\"\"\n",
        "    messages = state[\"messages\"]\n",
        "    current_message_count = len(messages)\n",
        "    if current_message_count < MESSAGE_SUMMARIZATION_THRESHOLD:\n",
        "        logger.debug(f\"Not summarizing conversation: {current_message_count}\")\n",
        "        return state\n",
        "\n",
        "    system_prompt = \"\"\"\n",
        "    You are a conversation summarizer. Create a concise summary of the previous\n",
        "    conversation between a user and a travel assistant.\n",
        "\n",
        "    The summary should:\n",
        "    1. Highlight key topics, preferences, and decisions\n",
        "    2. Include any specific trip details (destinations, dates, preferences)\n",
        "    3. Note any outstanding questions or topics that need follow-up\n",
        "    4. Be concise but informative\n",
        "\n",
        "    Format your summary as a brief narrative paragraph.\n",
        "    \"\"\"\n",
        "\n",
        "    message_content = \"\\n\".join(\n",
        "        [\n",
        "            f\"{'User' if isinstance(msg, HumanMessage) else 'Assistant'}: {msg.content}\"\n",
        "            for msg in messages\n",
        "        ]\n",
        "    )\n",
        "\n",
        "    # Invoke the summarizer\n",
        "    summary_messages = [\n",
        "        SystemMessage(content=system_prompt),\n",
        "        HumanMessage(\n",
        "            content=f\"Please summarize this conversation:\\n\\n{message_content}\"\n",
        "        ),\n",
        "    ]\n",
        "\n",
        "    summary_response = summarizer.invoke(summary_messages)\n",
        "\n",
        "    logger.info(f\"Summarized {len(messages)} messages into a conversation summary\")\n",
        "\n",
        "    summary_message = SystemMessage(\n",
        "        content=f\"\"\"\n",
        "        Summary of the conversation so far:\n",
        "\n",
        "        {summary_response.content}\n",
        "\n",
        "        Please continue the conversation based on this summary and the recent messages.\n",
        "        \"\"\"\n",
        "    )\n",
        "    remove_messages = [\n",
        "        RemoveMessage(id=msg.id) for msg in messages if msg.id is not None\n",
        "    ]\n",
        "\n",
        "    state[\"messages\"] = [  # type: ignore\n",
        "        *remove_messages,\n",
        "        summary_message,\n",
        "        state[\"messages\"][-1],\n",
        "    ]\n",
        "\n",
        "    return state.copy()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "dpzjQxXi1nfi"
      },
      "source": [
        "## Assemble the full graph\n",
        "\n",
        "🚧 It's time to assemble our graph for end-to-end agent execution. We will attach all three **nodes** we defined above."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "h6TvQaob1nfi"
      },
      "outputs": [],
      "source": [
        "from langgraph.graph import StateGraph, END\n",
        "\n",
        "workflow = StateGraph(RuntimeState)\n",
        "\n",
        "# Add nodes to the graph\n",
        "workflow.add_node(\"agent\", respond_to_user)\n",
        "workflow.add_node(\"execute_tools\", execute_tools)\n",
        "workflow.add_node(\"summarize_conversation\", summarize_conversation)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cYGE-DLuvKOO"
      },
      "source": [
        "Next, we will tie the nodes together using **edges** which control process flow. There is a conditional edge between the agent node and what comes next. What comes next is based on whether we need to handle + execute a tool call or proceed..."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "61Un_szhvKOO"
      },
      "outputs": [],
      "source": [
        "def decide_next_step(state):\n",
        "    latest_ai_message = next((m for m in reversed(state[\"messages\"]) if isinstance(m, AIMessage)), None)\n",
        "    if latest_ai_message and latest_ai_message.tool_calls:\n",
        "        return \"execute_tools\"\n",
        "    return \"summarize_conversation\"\n",
        "\n",
        "\n",
        "workflow.set_entry_point(\"agent\")\n",
        "workflow.add_conditional_edges(\n",
        "    \"agent\",\n",
        "    decide_next_step,\n",
        "    {\"execute_tools\": \"execute_tools\", \"summarize_conversation\": \"summarize_conversation\"},\n",
        ")\n",
        "workflow.add_edge(\"execute_tools\", \"agent\")\n",
        "workflow.add_edge(\"summarize_conversation\", END)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3L_OEc80vKOO"
      },
      "source": [
        "Compile the graph!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "metadata": {
        "id": "kuwdsVhYvKOO"
      },
      "outputs": [],
      "source": [
        "graph = workflow.compile(checkpointer=redis_saver)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "sQSfnQDK1nfo"
      },
      "source": [
        " ## Testing the Main Agent Loop\n",
        " \n",
        " Let's put our travel agent to work! The main loop handles the conversation flow:\n",
        " \n",
        " **Initialization**: Sets up a unique thread ID and empty message state\n",
        " \n",
        " **Input Processing**: Gets user input, handles empty inputs, and exit commands  \n",
        " \n",
        " **Message Flow**: Converts input to HumanMessage and streams through our workflow\n",
        " \n",
        "  **Response Generation**: Processes state and displays AI responses\n",
        " \n",
        " **Error Handling**: Catches errors and keeps the conversation flowing smoothly\n",
        " \n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 28,
      "metadata": {
        "id": "xD1BTjXY1nfp"
      },
      "outputs": [],
      "source": [
        "def main(thread_id: str = \"book_flight\", user_id: str = \"demo_user\"):\n",
        "    \"\"\"Main interaction loop for the travel agent\"\"\"\n",
        "\n",
        "    print(\"Welcome to the Travel Assistant! (Type 'exit' to quit)\")\n",
        "\n",
        "    config = RunnableConfig(configurable={\"thread_id\": thread_id, \"user_id\": user_id})\n",
        "    state = RuntimeState(messages=[])\n",
        "\n",
        "    while True:\n",
        "        user_input = input(\"\\nYou (type 'quit' to quit): \")\n",
        "\n",
        "        if not user_input:\n",
        "            continue\n",
        "\n",
        "        if user_input.lower() in [\"exit\", \"quit\"]:\n",
        "            print(\"Thank you for using the Travel Assistant. Goodbye!\")\n",
        "            break\n",
        "\n",
        "        state[\"messages\"].append(HumanMessage(content=user_input))\n",
        "\n",
        "        try:\n",
        "            # Process user input through the graph\n",
        "            for result in graph.stream(state, config=config, stream_mode=\"values\"):\n",
        "                state = RuntimeState(**result)\n",
        "\n",
        "            logger.debug(f\"# of messages after run: {len(state['messages'])}\")\n",
        "\n",
        "            # Find the most recent AI message, so we can print the response\n",
        "            ai_messages = [m for m in state[\"messages\"] if isinstance(m, AIMessage)]\n",
        "            if ai_messages:\n",
        "                message = ai_messages[-1].content\n",
        "            else:\n",
        "                logger.error(\"No AI messages after run\")\n",
        "                message = \"I'm sorry, I couldn't process your request properly.\"\n",
        "                # Add the error message to the state\n",
        "                state[\"messages\"].append(AIMessage(content=message))\n",
        "\n",
        "            print(f\"\\nAssistant: {message}\")\n",
        "\n",
        "        except Exception as e:\n",
        "            logger.exception(f\"Error processing request: {e}\")\n",
        "            error_message = \"I'm sorry, I encountered an error processing your request.\"\n",
        "            print(f\"\\nAssistant: {error_message}\")\n",
        "            # Add the error message to the state\n",
        "            state[\"messages\"].append(AIMessage(content=error_message))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "P51RdhnzfZa1"
      },
      "source": [
        "Before you try your own, take a look at the current conversation between Tyler and the travel agent. Notice the memory storage actions, the calls to the LLM, and also the conversation summarization that take place during the workflow!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "C5fg4PH97YGY",
        "outputId": "1a6fd03c-e0f5-46a8-9462-76f46260e901"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Enter a user ID: tyler\n",
            "Enter a thread ID: 123\n",
            "Welcome to the Travel Assistant! (Type 'exit' to quit)\n",
            "13:51:57 __main__ INFO   Starting memory consolidation for user tyler\n",
            "\n",
            "You (type 'quit' to quit): Hi I plan to go to singapore with my wife this summer. We love outdoors activities and trying new kinds of foods. Any good recommendations?\n",
            "13:52:30 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:52:30 __main__ INFO   Preparing to store memory: User plans to visit Singapore this summer with his wife and they love outdoor activities and trying new kinds of foods.\n",
            "13:52:31 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:52:31 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:52:31 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:52:31 __main__ INFO   Stored MemoryType.EPISODIC memory: User plans to visit Singapore this summer with his wife and they love outdoor activities and trying new kinds of foods.\n",
            "13:52:37 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "\n",
            "Assistant: Singapore is a fantastic destination for outdoor activities and culinary adventures! Here are some recommendations that you and your wife might enjoy:\n",
            "\n",
            "### Outdoor Activities:\n",
            "1. **Gardens by the Bay**: Explore the stunning gardens and the iconic Supertree Grove. You can also walk along the OCBC Skyway for a breathtaking view of the Marina Bay area.\n",
            "\n",
            "2. **Sentosa Island**: Enjoy a day at the beach, try zip-lining, or explore the numerous attractions like Universal Studios Singapore.\n",
            "\n",
            "3. **MacRitchie Reservoir**: Go for a hike along the MacRitchie Trails and experience the TreeTop Walk, a suspension bridge spanning the forest canopy.\n",
            "\n",
            "4. **Pulau Ubin**: Rent a bicycle and explore this rustic island. It's a great place to enjoy nature and see what Singapore was like in the past.\n",
            "\n",
            "### Food Experiences:\n",
            "1. **Hawker Centers**: Visit places like Maxwell Food Centre or Lau Pa Sat to try local dishes such as Hainanese chicken rice, laksa, and chili crab.\n",
            "\n",
            "2. **Peranakan Cuisine**: Try something different with Peranakan or Nyonya food, which is a blend of Chinese and Malay culinary traditions. \n",
            "\n",
            "3. **Jumbo Seafood**: Known for their chili crab, this is a must-try for seafood lovers. There are several locations around the city.\n",
            "\n",
            "4. **Food Tours**: Consider joining a food tour to explore the diverse culinary scene in Singapore and learn about the history and culture behind each dish.\n",
            "\n",
            "Feel free to ask if you need more details or have specific interests!\n",
            "\n",
            "You (type 'quit' to quit): Excellent thank you. I would love help booking flights. What are the best routes typically flown from Atlanta to Singapore?\n",
            "13:53:24 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "\n",
            "Assistant: Flying from Atlanta to Singapore usually involves at least one stopover, as there are no direct flights. Here are some of the best routes typically flown:\n",
            "\n",
            "1. **Atlanta (ATL) to Singapore (SIN) via Tokyo (NRT/HND)**:\n",
            "   - Airlines: Delta Air Lines, Japan Airlines\n",
            "   - This route often involves a stop in Tokyo, which can be a great opportunity to explore Japan if you have a long layover.\n",
            "\n",
            "2. **Atlanta (ATL) to Singapore (SIN) via Seoul (ICN)**:\n",
            "   - Airlines: Korean Air, Delta Air Lines\n",
            "   - A stopover in Seoul offers another chance for a brief visit in South Korea.\n",
            "\n",
            "3. **Atlanta (ATL) to Singapore (SIN) via Doha (DOH)**:\n",
            "   - Airline: Qatar Airways\n",
            "   - Qatar Airways offers a stop in Doha, which is known for its luxurious airport and facilities.\n",
            "\n",
            "4. **Atlanta (ATL) to Singapore (SIN) via Dubai (DXB)**:\n",
            "   - Airline: Emirates\n",
            "   - Stopping in Dubai can be a fantastic experience with plenty of attractions to explore.\n",
            "\n",
            "5. **Atlanta (ATL) to Singapore (SIN) via Frankfurt (FRA) or Munich (MUC)**:\n",
            "   - Airlines: Lufthansa, Singapore Airlines\n",
            "   - A European stopover provides a different cultural experience.\n",
            "\n",
            "When booking flights, it's a good idea to consider the total travel time, layover duration, and the amenities offered by the airlines. Let me know if you would like assistance with booking or have any preferences, such as specific airlines or layover cities!\n",
            "\n",
            "You (type 'quit' to quit): Ok great. As you know we like Delta. But we also prefer to sit first class if possible. Do these kinds of routes offer upgrades normally?\n",
            "13:53:32 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:53:32 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:53:37 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:53:41 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:53:41 __main__ INFO   Summarized 6 messages into a conversation summary\n",
            "\n",
            "Assistant: Since you prefer Delta Air Lines, you might consider the following route:\n",
            "\n",
            "### Atlanta (ATL) to Singapore (SIN) via Tokyo (NRT/HND):\n",
            "- **Airlines**: Delta Air Lines, Japan Airlines\n",
            "- **Class Options**: Delta offers Delta One, their premium cabin, which includes lie-flat seats, premium dining, and other luxury amenities.\n",
            "\n",
            "Delta typically allows for upgrades to first-class or their premium Delta One cabin, especially if you're a SkyMiles member or have elite status. Upgrades can often be requested at the time of booking or closer to the departure date, depending on availability.\n",
            "\n",
            "For the best chance of securing an upgrade, consider:\n",
            "- Booking early and using any available miles or upgrade certificates.\n",
            "- Checking the Delta app or website regularly for upgrade availability.\n",
            "- Contacting Delta customer service if you have specific requests or need assistance with your booking.\n",
            "\n",
            "Let me know if you need help with booking or have any other questions!\n",
            "\n",
            "You (type 'quit' to quit): Let's hold on booking for now. Back to activities. Based on what you know about me, what do you think we should do? Design the perfect Sunday for me and my wife in Singapore.\n",
            "13:54:05 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:54:06 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:54:13 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "\n",
            "Assistant: Here's a perfect Sunday itinerary in Singapore, tailored to your love for outdoor activities and culinary experiences:\n",
            "\n",
            "### Morning:\n",
            "- **Breakfast at Tiong Bahru Bakery**: Start your day with a delicious breakfast at this popular bakery known for its croissants and artisanal coffee.\n",
            "- **Visit Gardens by the Bay**: Spend the morning exploring this iconic attraction. Don't miss the Supertree Grove and Cloud Forest Dome for a mix of nature and futuristic architecture.\n",
            "\n",
            "### Midday:\n",
            "- **Lunch at Lau Pa Sat Hawker Centre**: Head to this historic food market for a taste of Singapore's diverse street food. Try local favorites like Hainanese chicken rice, satay, and laksa.\n",
            "- **Stroll Along Marina Bay**: Enjoy a leisurely walk along Marina Bay and take in the stunning skyline views. You can also visit the Merlion Park for some iconic photo opportunities.\n",
            "\n",
            "### Afternoon:\n",
            "- **Biking at East Coast Park**: Rent a bike and enjoy a ride along the scenic coastline. The park offers a beautiful setting for outdoor activities and relaxation.\n",
            "- **Explore Katong and Joo Chiat**: Discover the colorful shophouses and Peranakan culture in these charming neighborhoods. You can also stop by for some traditional Peranakan snacks.\n",
            "\n",
            "### Evening:\n",
            "- **Dinner at a Rooftop Restaurant**: End your day with a romantic dinner at a rooftop restaurant like Level33 or Ce La Vi, offering panoramic views of the city skyline.\n",
            "- **Night Safari at Singapore Zoo**: If you're up for some adventure, consider the Night Safari for a unique experience of seeing nocturnal animals in their natural habitat.\n",
            "\n",
            "This itinerary combines your love for the outdoors with Singapore's rich culinary scene, ensuring a memorable day for you and your wife. Let me know if you'd like more details or adjustments!\n",
            "\n",
            "You (type 'quit' to quit): Great as long as there are no shellfish items featured as my wife is very allergic.\n",
            "13:54:52 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:54:52 __main__ INFO   Preparing to store memory: User's wife is allergic to shellfish.\n",
            "13:54:58 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:54:58 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:54:58 __main__ INFO   Stored MemoryType.EPISODIC memory: User's wife is allergic to shellfish.\n",
            "13:55:00 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:55:03 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:55:03 __main__ INFO   Summarized 6 messages into a conversation summary\n",
            "\n",
            "Assistant: I've noted that your wife is allergic to shellfish. When dining at places like hawker centers or restaurants, it's a good idea to inform the staff about her allergy to ensure a safe dining experience. \n",
            "\n",
            "If you need more personalized recommendations or adjustments to your itinerary, feel free to ask!\n",
            "\n",
            "You (type 'quit' to quit): Excellent. Back to booking flights. For our preferred airline, will you recommend best route to get there from the US?\n",
            "13:55:32 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:55:32 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:55:34 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "\n",
            "Assistant: Given your preference for Delta Air Lines, I recommend flying from Atlanta (ATL) to Singapore (SIN) with a layover in Tokyo (NRT). Delta offers convenient connections through Tokyo, and this route is popular for travelers heading to Singapore.\n",
            "\n",
            "Would you like me to assist with finding specific flight options or any other details regarding the booking?\n",
            "\n",
            "You (type 'quit' to quit):  Sounds great. We will go with this one. I havent been to Singapore since I was 15 years old when I went with my family during my dads business trip. We had such a great time. I think you are going to help us have a fantastic trip!!\n",
            "13:55:52 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:55:52 __main__ INFO   Preparing to store memory: User visited Singapore at the age of 15 with their family during their dad's business trip and had a great time.\n",
            "13:55:52 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:55:52 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n",
            "13:55:52 __main__ INFO   Stored MemoryType.EPISODIC memory: User visited Singapore at the age of 15 with their family during their dad's business trip and had a great time.\n",
            "13:55:54 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:55:57 httpx INFO   HTTP Request: POST https://api.openai.com/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
            "13:55:57 __main__ INFO   Summarized 6 messages into a conversation summary\n",
            "\n",
            "Assistant: I'm thrilled to be part of planning your trip back to Singapore! It sounds like you have fond memories from your last visit, and I'm here to help make this trip just as memorable. If you need any more assistance with flights or have questions about your itinerary, just let me know!\n",
            "\n",
            "You (type 'quit' to quit): quit\n",
            "Thank you for using the Travel Assistant. Goodbye!\n"
          ]
        }
      ],
      "source": [
        "try:\n",
        "    user_id = input(\"Enter a user ID: \") or \"demo_user\"\n",
        "    thread_id = input(\"Enter a thread ID: \") or \"demo_thread\"\n",
        "except Exception:\n",
        "    # If we're running in CI, we don't have a terminal to input from, so just exit\n",
        "    exit()\n",
        "else:\n",
        "    main(thread_id, user_id)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "xmFwUzVo2qxB"
      },
      "source": [
        "Let's review what the agent learned about me during the process!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-txziZxw2jik",
        "outputId": "1ac4bf0b-ec10-4fd2-ae28-8568e9be5829"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "13:56:11 httpx INFO   HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "['Long-term memories:',\n",
              " '- [MemoryType.EPISODIC] User plans to visit Singapore this summer with his wife and they love outdoor activities and trying new kinds of foods.',\n",
              " \"- [MemoryType.EPISODIC] User visited Singapore at the age of 15 with their family during their dad's business trip and had a great time.\",\n",
              " '- [MemoryType.EPISODIC] I like flying on Delta when possible',\n",
              " \"- [MemoryType.EPISODIC] User's wife is allergic to shellfish.\"]"
            ]
          },
          "execution_count": 22,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "res = retrieve_memories_tool.invoke({\"query\": \"Travel, activity, and dietary preferences\", \"memory_type\": [\"episodic\", \"semantic\"]})\n",
        "res.split(\"\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "M2UrJxte5HRT"
      },
      "source": [
        "Don't forget, we have the RedisVL index we can use to manually query or work with as needed:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "qYm3HQj54WPX",
        "outputId": "5db66cb4-e07b-40dd-e4ae-35fed24e283d"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "4"
            ]
          },
          "execution_count": 23,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from redisvl.query import CountQuery\n",
        "\n",
        "# count total long-term memories in Redis\n",
        "long_term_memory_index.query(CountQuery())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "recap-summary"
      },
      "source": [
        "___\n",
        "\n",
        "# 🎓 Recap\n",
        "\n",
        "You've now learned the fundamentals from scratch and built a **production-ready memory-enabled AI agent** from the ground up. Let's recap the key accomplishments:\n",
        "\n",
        "## 🏗️ What we built\n",
        "\n",
        "1. ✅ **Dual-Memory Architecture**: Short-term conversation state + long-term persistent knowledge with LangGraph and Redis\n",
        "2. ✅ **Vector-Powered Memory**: Semantic search using RedisVL\n",
        "3. ✅ **Smart Deduplication**: Prevents storing similar memories multiple times  \n",
        "4. ✅ **Tool-Based Memory Management**: LLM controls when to store/retrieve memories  \n",
        "5. ✅ **Conversation Summarization**: Automatic context window management  \n",
        "\n",
        "**Why Redis?**\n",
        "\n",
        "- **Performance**: Sub-millisecond memory retrieval at scale  \n",
        "- **Versatility**: Handles both structured state (checkpoints) and unstructured data (vectors)  \n",
        "- **Production-Ready**: Built-in persistence, clustering, and high availability  \n",
        "- **Developer Experience**: Rich ecosystem with tools like RedisVL and AI framework integrations  \n",
        "\n",
        "## 🔧 Alternative memory dev frameworks\n",
        "\n",
        "While this tutorial shows hands-on implementation, consider these frameworks for faster development:\n",
        "\n",
        "- **[LangMem](https://github.com/langchain-ai/langmem)**: LangChain's official memory framework\n",
        "- **[Mem0](https://github.com/mem0-ai/mem0)**: Dedicated memory layer for AI applications\n",
        "\n",
        "**When to Use Each Approach:**\n",
        "- **Custom Implementation** (this tutorial): Maximum control, specific requirements, learning\n",
        "- **LangMem**: LangChain ecosystem integration, rapid prototyping\n",
        "- **Mem0**: Multi-application memory sharing, enterprise features\n",
        "\n",
        "## 🔄 Next Steps\n",
        "\n",
        "You now have the foundation to build sophisticated, memory-enabled AI agents that feel truly intelligent and personalized.\n",
        "\n",
        " **Need Expert Guidance?**\n",
        "# \n",
        " [Schedule a consultation](https://redis.io/meeting/?utm_source=nir&utm_medium=cpa&utm_campaign=2025-06-ai_in_production&utm_content=cr-all_contact_us_forms-7013z000000evRk) with our Redis experts to optimize your agent architecture and implementation."
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": ".venv",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.11.11"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
